web/src/routes/image/[id]/+page.svelte (view raw)
1<script lang="ts">
2 import { onMount } from 'svelte';
3 import { getImage, imageDownloadUrl, thumbnailUrl, type ImageDetail } from '$lib/api';
4
5 let { params } = $props();
6 let id = $derived(parseInt(params.id));
7
8 let img = $state<ImageDetail | null>(null);
9 let loading = $state(true);
10 let error = $state('');
11
12 onMount(async () => {
13 try {
14 img = await getImage(id);
15 } catch (err) {
16 error = err instanceof Error ? err.message : 'Failed to load image';
17 } finally {
18 loading = false;
19 }
20 });
21
22 function formatSize(bytes: number): string {
23 if (bytes < 1024) return bytes + ' B';
24 if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
25 return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
26 }
27
28 function formatDate(s: string): string {
29 return new Date(s).toLocaleString();
30 }
31</script>
32
33{#if loading}
34 <p class="state-msg">Loading...</p>
35{:else if error}
36 <p class="state-msg error">{error}</p>
37{:else if img}
38 <div class="page-head">
39 <h1>{img.filename}</h1>
40 <div class="head-line"></div>
41 </div>
42
43 <div class="layout">
44 <div class="preview">
45 {#if img.thumbnail_path}
46 <img src={imageDownloadUrl(img.id)} alt={img.filename} />
47 {:else}
48 <div class="no-thumb">NO THUMBNAIL</div>
49 {/if}
50 </div>
51
52 <div class="meta">
53 <div class="meta-block uploader-block">
54 <span class="meta-label">UPLOADED BY</span>
55 <div class="uploader-row">
56 {#if img.avatar_url}
57 <img src={img.avatar_url} alt="" class="avatar-img" />
58 {/if}
59 <span class="meta-value">{img.uploader_username}</span>
60 </div>
61 </div>
62 <div class="meta-block">
63 <span class="meta-label">ID</span>
64 <span class="meta-value">#{img.id}</span>
65 </div>
66 <div class="meta-block">
67 <span class="meta-label">DIMENSIONS</span>
68 <span class="meta-value">{img.width} × {img.height}</span>
69 </div>
70 <div class="meta-block">
71 <span class="meta-label">FILE SIZE</span>
72 <span class="meta-value">{formatSize(img.file_size)}</span>
73 </div>
74 <div class="meta-block">
75 <span class="meta-label">TYPE</span>
76 <span class="meta-value">{img.mime_type}</span>
77 </div>
78 <div class="meta-block">
79 <span class="meta-label">UPLOADED</span>
80 <span class="meta-value">{formatDate(img.created_at)}</span>
81 </div>
82 <div class="meta-block tags-block">
83 <span class="meta-label">TAGS</span>
84 <div class="tag-pills">
85 {#if img.tags_detail.length > 0}
86 {#each img.tags_detail as t (t.tag)}
87 <a href="/browse?tag={encodeURIComponent(t.tag)}" class="tag-pill" title="Used {t.usage_count} time{t.usage_count === 1 ? '' : 's'}">
88 <span class="tag-name">{t.tag}</span>
89 <span class="tag-count">{t.usage_count}</span>
90 </a>
91 {/each}
92 {:else}
93 <span class="none">none</span>
94 {/if}
95 </div>
96 </div>
97
98 <a href={imageDownloadUrl(img.id)} class="btn" download={img.filename}>
99 DOWNLOAD ORIGINAL
100 </a>
101 </div>
102 </div>
103
104 <p class="back"><a href="/browse">← BACK TO BROWSE</a></p>
105{/if}
106
107<style>
108 .state-msg {
109 padding: 2rem 0;
110 font-size: 1.1rem;
111 color: #888;
112 }
113 .error {
114 color: #c62828;
115 }
116
117 .page-head {
118 margin-bottom: 1.5rem;
119 }
120 .page-head h1 {
121 word-break: break-all;
122 }
123 .head-line {
124 height: 3px;
125 background: #c62828;
126 width: 3rem;
127 margin-top: 0.4rem;
128 }
129
130 .layout {
131 display: flex;
132 gap: 2rem;
133 flex-wrap: wrap;
134 }
135 .preview {
136 flex: 1;
137 min-width: 300px;
138 background: #1a1a1a;
139 display: flex;
140 align-items: center;
141 justify-content: center;
142 }
143 .preview img {
144 display: block;
145 width: 100%;
146 height: auto;
147 max-height: 70vh;
148 object-fit: contain;
149 }
150 .no-thumb {
151 padding: 4rem 2rem;
152 color: #555;
153 font-family: 'Oswald', sans-serif;
154 font-weight: 500;
155 font-size: 0.9rem;
156 letter-spacing: 0.12em;
157 }
158
159 .meta {
160 width: 260px;
161 flex-shrink: 0;
162 display: flex;
163 flex-direction: column;
164 gap: 1rem;
165 }
166 .meta-block {
167 display: flex;
168 flex-direction: column;
169 gap: 0.15rem;
170 }
171 .meta-label {
172 font-family: 'Oswald', sans-serif;
173 font-weight: 500;
174 font-size: 0.7rem;
175 letter-spacing: 0.15em;
176 color: #999;
177 }
178 .meta-value {
179 font-family: 'Inter', sans-serif;
180 font-size: 0.9rem;
181 font-weight: 600;
182 color: #1a1a1a;
183 }
184
185 .uploader-row {
186 display: flex;
187 align-items: center;
188 gap: 0.4rem;
189 }
190 .avatar-img {
191 width: 1.5rem;
192 height: 1.5rem;
193 object-fit: cover;
194 flex-shrink: 0;
195 }
196
197 .tags-block {
198 flex: 1;
199 }
200 .tag-pills {
201 display: flex;
202 flex-wrap: wrap;
203 gap: 0.35rem;
204 }
205 .tag-pill {
206 display: inline-flex;
207 align-items: center;
208 gap: 0.3rem;
209 background: #1a1a1a;
210 padding: 0.25rem 0.5rem 0.25rem 0.65rem;
211 font-family: 'Inter', sans-serif;
212 font-size: 0.78rem;
213 font-weight: 500;
214 color: #eee;
215 text-decoration: none;
216 transition: background 0.15s;
217 }
218 .tag-pill:hover {
219 background: #c62828;
220 text-decoration: none;
221 }
222 .tag-name {
223 font-weight: 600;
224 }
225 .tag-count {
226 background: rgba(255, 255, 255, 0.12);
227 padding: 0.05rem 0.35rem;
228 font-size: 0.68rem;
229 font-weight: 600;
230 color: rgba(255, 255, 255, 0.65);
231 min-width: 1.1rem;
232 text-align: center;
233 }
234 .tag-pill:hover .tag-count {
235 background: rgba(0, 0, 0, 0.2);
236 }
237 .none {
238 color: #bbb;
239 font-size: 0.85rem;
240 }
241
242 .btn {
243 display: inline-flex;
244 align-items: center;
245 align-self: flex-start;
246 padding: 0.55rem 1.25rem;
247 background: #c62828;
248 color: #fff;
249 border: none;
250 font-family: 'Oswald', sans-serif;
251 font-weight: 500;
252 font-size: 0.8rem;
253 letter-spacing: 0.1em;
254 text-decoration: none;
255 text-transform: uppercase;
256 cursor: pointer;
257 transition: background 0.15s;
258 margin-top: 0.5rem;
259 }
260 .btn:hover {
261 background: #b71c1c;
262 text-decoration: none;
263 }
264
265 .back {
266 margin-top: 2rem;
267 }
268 .back a {
269 font-family: 'Oswald', sans-serif;
270 font-weight: 500;
271 font-size: 0.85rem;
272 letter-spacing: 0.1em;
273 }
274</style>